﻿namespace SecutityLib
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using System.Text;

    /// <summary>
    /// MD5 technique
    /// </summary>
    public class MD5 : EncodingDecodingTech
    {
        /// <summary>
        /// reg a
        /// </summary>
        private static uint a = 0x67452301;

        /// <summary>
        /// reg b
        /// </summary>
        private static uint b = 0xEFCDAB89;

        /// <summary>
        /// reg c
        /// </summary>
        private static uint c = 0x98BADCFE;

        /// <summary>
        /// reg d
        /// </summary>
        private static uint d = 0x10325476;

        /// <summary>
        /// shift values of F
        /// </summary>
        private int[] shiftOfF = { 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22 };

        /// <summary>
        /// shift values of G
        /// </summary>
        private int[] shiftOfG = { 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20 };

        /// <summary>
        /// shift values of H
        /// </summary>
        private int[] shiftOfH = { 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23 };

        /// <summary>
        /// shift values of I
        /// </summary>
        private int[] shiftOfI = { 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21 };

        /// <summary>
        /// first padding 
        /// </summary>
        private bool firstPadding;

        /// <summary>
        /// Encodes the specified plain text.
        /// </summary>
        /// <param name="plainText">The plain text.</param>
        /// <param name="key">The key.</param>
        /// <returns>
        /// cipher text
        /// </returns>
        public override string Encode(string plainText, string key)
        {
            this.firstPadding = true;

            a = 0x67452301;
            b = 0xEFCDAB89;
            c = 0x98BADCFE;
            d = 0x10325476;

            int length = plainText.Length;
            byte[] message = this.GetBytesOfMessage(plainText);
            int numberOfPaddingBits = this.GetNumberOFPaddingBits(message.Length * 8);
            byte[] wholeString = this.AddPaddingBits(message, numberOfPaddingBits);
            uint[] wordsOfMessage = this.GetWordsFromBytes(wholeString);
            int cnt = wordsOfMessage.Length / 16;
            uint[] tempRoundWords = new uint[16];
            for (int i = 0; i < cnt; i++)
            {
                uint tempA = a;
                uint tempB = b;
                uint tempC = c;
                uint tempD = d;
                wordsOfMessage.CopyTo(tempRoundWords, i * 16);
                this.ApplyRound(tempRoundWords);

                a = this.Add(a, tempA);
                b = this.Add(b, tempB);
                c = this.Add(c, tempC);
                d = this.Add(d, tempD);
            }

            return this.GetFinalCipher();
        }

        /// <summary>
        /// Decodes the specified plain text.
        /// </summary>
        /// <param name="cipherText">The cipher text.</param>
        /// <param name="key">The key.</param>
        /// <returns>
        /// cipher text
        /// </returns>
        public override string Decode(string cipherText, string key)
        {
            throw new Exception("MD5 is a hashing algorithm so it's one directional, cannot apply decoding");
        }

        /// <summary>
        /// Applies the round.
        /// </summary>
        /// <param name="tempRoundWords">The temp round words.</param>
        private void ApplyRound(uint[] tempRoundWords)
        {
            this.ApplySubRound('F', tempRoundWords);
            this.ApplySubRound('G', tempRoundWords);
            this.ApplySubRound('H', tempRoundWords);
            this.ApplySubRound('I', tempRoundWords);
        }

        /// <summary>
        /// Gets the number OF padding bits.
        /// </summary>
        /// <param name="messageLengthInBits">The message length in bits.</param>
        /// <returns>no of padding bits </returns>
        private int GetNumberOFPaddingBits(int messageLengthInBits)
        {
            // msg+1+p mod 512 = 448
            // p mod 512 = 447-msg 
            int z = 447 - messageLengthInBits;
            int p = 0;
            while (z < 0)
            {
                z += 512;
            }

            for (int i = 0; i < 512; i++)
            {
                if (i % 512 == z)
                {
                    p = i;
                    break;
                }
            }

            return p + 1;
        }

        /// <summary>
        /// Gets the bytes of message.
        /// </summary>
        /// <param name="message">The message.</param>
        /// <returns>message bytes</returns>
        private byte[] GetBytesOfMessage(string message)
        {
            byte[] temp = new byte[message.Length];

            if (message[0] == '0' && message[1] == 'x')
            {
                return this.GetBytesFromHexValue(message);
            }

            for (int i = 0; i < message.Length; i++)
            {
                temp[i] = (byte)message[i];
            }

            return temp;
        }

        /// <summary>
        /// Gets the bytes from hex value.
        /// </summary>
        /// <param name="message">The message.</param>
        /// <returns>hex values in byte format</returns>
        private byte[] GetBytesFromHexValue(string message)
        {
            message = message.Substring(2);
            byte[] temp = new byte[message.Length / 2];
            if (message.Length % 2 == 0)
            {
                for (int i = 0; i < temp.Length; i += 2)
                {
                    temp[i] = byte.Parse(message.Substring(i * 2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
                }

                return temp;
            }
            else
            {
                message += "8";
                this.firstPadding = false;
                for (int i = 0; i < temp.Length; i += 2)
                {
                    temp[i] = byte.Parse(message.Substring(i * 2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
                }

                return temp;
            }
        }

        /// <summary>
        /// Adds the padding bits.
        /// </summary>
        /// <param name="message">The message.</param>
        /// <param name="numberOfPaddingBits">The number of padding bits.</param>
        /// <returns>the whole message</returns>
        private byte[] AddPaddingBits(byte[] message, int numberOfPaddingBits)
        {
            int numberOfBytesForPadding = numberOfPaddingBits / 8;
            byte[] paddingBytes = new byte[numberOfBytesForPadding];

            for (int i = 0; i < numberOfBytesForPadding; i++)
            {
                if (i == 0 && this.firstPadding)
                {
                    paddingBytes[i] = 0x80;
                }
                else
                {
                    paddingBytes[i] = 0x00;
                }
            }

            byte[] bytesOfMessageLength = new byte[8];

            bytesOfMessageLength = BitConverter.GetBytes(message.Length * 8);

            int cnt = 0;

            byte[] temp = new byte[message.Length + numberOfBytesForPadding + 8];
            message.CopyTo(temp, 0);
            cnt += message.Length;
            paddingBytes.CopyTo(temp, message.Length);
            cnt += paddingBytes.Length;

            for (int i = 0; i < 4; i++)
            {
                temp[cnt++] = bytesOfMessageLength[i];
            }

            return temp;
        }

        /// <summary>
        /// Gets the words from bytes.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <returns>words from bytes</returns>
        private uint[] GetWordsFromBytes(byte[] x)
        {
            uint[] temp = new uint[x.Length / 4];
            byte[] tempWord = new byte[4];

            int cnt = 0;
            for (int i = 0; i < temp.Length; i++)
            {
                tempWord[0] = x[cnt++];
                tempWord[1] = x[cnt++];
                tempWord[2] = x[cnt++];
                tempWord[3] = x[cnt++];

                temp[i] = BitConverter.ToUInt32(tempWord, 0);
            }

            return temp;
        }

        /// <summary>
        /// Applies the sub round.
        /// </summary>
        /// <param name="charFGHI">The FGHI.</param>
        /// <param name="x16word">The x16word.</param>
        private void ApplySubRound(char charFGHI, uint[] x16word)
        {
            uint tempT;
            uint tempX;

            int indexOfRound = 0;
            int indexOfW;
            if (charFGHI == 'G' || charFGHI == 'g')
            {
                indexOfRound = 16;
            }
            else if (charFGHI == 'H' || charFGHI == 'h')
            {
                indexOfRound = 32;
            }
            else if (charFGHI == 'I' || charFGHI == 'i')
            {
                indexOfRound = 48;
            }

            uint tempA;
            uint tempB;
            uint tempC;
            uint tempD;
            uint accum;
            for (int i = 0; i < 16; i++)
            {
                indexOfW = this.GetIndexOfRound(charFGHI, indexOfRound);

                tempA = a;
                tempB = b;
                tempC = c;
                tempD = d;

                tempT = (uint)(Math.Pow(2, 32) * Math.Abs(Math.Sin(indexOfRound + 1)));
                tempX = x16word[indexOfW];
                uint outputOfLogicalFunction = this.GetOutput(charFGHI);

                d = tempC;
                c = tempB;
                a = tempD;

                accum = tempA;
                accum = this.Add(accum, outputOfLogicalFunction);
                accum = this.Add(accum, tempX);
                accum = this.Add(accum, tempT);
                accum = this.ShiftWord(charFGHI, i, accum);
                b = this.Add(accum, tempB);

                indexOfRound++;
            }
        }

        /// <summary>
        /// Gets the output.
        /// </summary>
        /// <param name="charFGHI">The FGHI.</param>
        /// <returns>final 128 bits output</returns>
        private uint GetOutput(char charFGHI)
        {
            if (charFGHI == 'F' || charFGHI == 'f')
            {
                return this.F();
            }
            else if (charFGHI == 'G' || charFGHI == 'g')
            {
                return this.G();
            }
            else if (charFGHI == 'H' || charFGHI == 'h')
            {
                return this.H();
            }
            else
            {
                return this.I();
            }
        }

        /// <summary>
        /// Gets the index of round.
        /// </summary>
        /// <param name="charFGHI">The FGHI.</param>
        /// <param name="i">The i.</param>
        /// <returns>index of the round (P(i))</returns>
        private int GetIndexOfRound(char charFGHI, int i)
        {
            if (charFGHI == 'F' || charFGHI == 'f')
            {
                return i;
            }
            else if (charFGHI == 'G' || charFGHI == 'g')
            {
                return (1 + (5 * i)) % 16;
            }
            else if (charFGHI == 'H' || charFGHI == 'h')
            {
                return (5 + (3 * i)) % 16;
            }
            else
            {
                return (7 * i) % 16;
            }
        }

        /// <summary>
        /// Adds the specified one.
        /// </summary>
        /// <param name="one">The one.</param>
        /// <param name="two">The two.</param>
        /// <returns>added mod 2^32</returns>
        private uint Add(uint one, uint two)
        {
            return (uint)((one + two) & 0xFFFFFFFF);
        }

        /// <summary>
        /// F round
        /// </summary>
        /// <returns>F round result</returns>
        private uint F()
        {
            return (b & c) | (~b & d);
        }

        /// <summary>
        /// G round .
        /// </summary>
        /// <returns>G round result</returns>
        private uint G()
        {
            return (b & d) | (c & ~d);
        }

        /// <summary>
        /// H round 
        /// </summary>
        /// <returns>H round result</returns>
        private uint H()
        {
            return b ^ c ^ d;
        }

        /// <summary>
        /// I round 
        /// </summary>
        /// <returns>I round result</returns>
        private uint I()
        {
            return c ^ (b | ~d);
        }

        /// <summary>
        /// Shifts the word.
        /// </summary>
        /// <param name="charFGHI">The FGHI.</param>
        /// <param name="roundNumber">The round number.</param>
        /// <param name="value">The value.</param>
        /// <returns>shifted word</returns>
        private uint ShiftWord(char charFGHI, int roundNumber, uint value)
        {
            int n = 0;
            if (charFGHI == 'F' || charFGHI == 'f')
            {
                n = this.shiftOfF[roundNumber];
            }
            else if (charFGHI == 'G' || charFGHI == 'g')
            {
                n = this.shiftOfG[roundNumber];
            }
            else if (charFGHI == 'H' || charFGHI == 'h')
            {
                n = this.shiftOfH[roundNumber];
            }
            else
            {
                n = this.shiftOfI[roundNumber];
            }

            return (uint)((value << n) | (value >> (32 - n)));
        }

        /// <summary>
        /// Gets the final cipher.
        /// </summary>
        /// <returns>final 128 bit result</returns>
        private string GetFinalCipher()
        {
            try
            {
                string text = string.Empty;
                byte[] temp = BitConverter.GetBytes(a);
                text += BitConverter.ToString(temp, 0);
                text += " ";

                temp = BitConverter.GetBytes(b);
                text += BitConverter.ToString(temp, 0);
                text += " ";

                temp = BitConverter.GetBytes(c);

                text += BitConverter.ToString(temp, 0);
                text += " ";

                temp = BitConverter.GetBytes(d);
                text += BitConverter.ToString(temp, 0);
                text += " ";

                return text;
            }
            catch
            {
                throw new Exception("Error converting bytes to text");
            }
        }
    }
}
